
#pragma once

#include "gl/glew.h"
#include "mymath.h"

#include <string>
#include <map>

using namespace std;

//////////////////////////////////////////////////////////////////////////
// Uniforms related
// Disadvantage: when buffer gets dirty it gets fully reuploaded. It's possible
// to save on this and reupload only what's necessary. Some coding is required for this.
// However, usually uniform buffers aren't too large and reuploading everything should work fine.
#include "shaderDefs.h"

class UniformBlock {
protected:
    mutable GLvoid* m_data;
    mutable bool m_dirty;
    GLsizeiptr m_size;
    GLuint m_id;
    string m_name;

    // In some cases purpose might become stream_draw (material properties for instance)
    GLenum m_purpose;

    UniformBlock(const char* name, int size, void* data, GLenum purpose);

public:
    virtual ~UniformBlock();

    void BindToProgram(GLuint program, GLuint bindingSlot) const;
    void BindToState(GLuint bindingSlot) const;
    virtual void Update() const;
    virtual void SetData(void* data);
    virtual void SetData(void* data) const;
    const string& Name() const { return m_name; }
};
typedef map<string, const UniformBlock*> UniformMap;


class LightsBlock : public UniformBlock {
public:
    struct SLight {
        vec4 m_pos;
        vec4 m_color;
    };

    LightsBlock(const char* name)
        : UniformBlock(name, sizeof(SLight)*MAX_LIGHTS, NULL, GL_DYNAMIC_DRAW)
    {
        m_lights = (SLight*)_aligned_malloc(m_size, 16);
        m_data=m_lights;
    }
    ~LightsBlock()
    {
        _aligned_free(m_lights);
    }

    const vec4& LightPos(int light) const { return m_lights[light].m_pos; }
    const vec4& LightColor(int light) const { return m_lights[light].m_color; }
    vec4& LightPos(int light) { m_dirty=true; return m_lights[light].m_pos; }
    vec4& LightColor(int light) { m_dirty=true; return m_lights[light].m_color; }

private:
    SLight *m_lights;
};

class MatrixBlock : public UniformBlock {
    struct SMatrices {
        mat4 Projection;
        mat4 Camera;
        float NormalC[12];
    } *m_Matrices;

public:
    MatrixBlock(const char* name)
        : UniformBlock(name, sizeof(SMatrices), NULL, GL_DYNAMIC_DRAW)
    {
        m_Matrices = (SMatrices*)_aligned_malloc(m_size, 16);
        m_data=m_Matrices;
    }
    ~MatrixBlock()
    {
        _aligned_free(m_Matrices);
    }

    void Update() const
    {
        if(m_dirty)
            m_Matrices->Camera.NormalMatrix(m_Matrices->NormalC);
        UniformBlock::Update();
    }

    const mat4& Camera() const { return m_Matrices->Camera; }
    const mat4& Projection() const { return m_Matrices->Projection; }
    mat4& Camera() { m_dirty=true; return m_Matrices->Camera; }
    mat4& Projection() { m_dirty=true; return m_Matrices->Projection; }
};

class ModelBlock : public UniformBlock {
public:
    struct SModel {
        mat4 ModelView;
        float NormalM[12];
    };

    ModelBlock(const char* name)
        : UniformBlock(name, sizeof(SModel), NULL, GL_STREAM_DRAW)
        , m_Model(NULL)
    {
    }
    ~ModelBlock()
    {
    }

    void SetData(void* data) { throw "Can't copy data to this block"; }
    void SetData(void* data) const { m_data=data; m_dirty=true; }

private:
    const SModel* m_Model;
};

class MaterialBlock : public UniformBlock {
public:
    struct SMaterial {
        vec4 diffuse;
        vec4 extra;
    };

    MaterialBlock(const char* name)
        : UniformBlock(name, sizeof(SMaterial), NULL, GL_STREAM_DRAW)
        , m_material(NULL)
    {}
    ~MaterialBlock()
    {}

    void SetData(void* data) { throw "Can't copy data to this block"; }
    void SetData(void* data) const { m_data=data; m_dirty=true; }

private:
    const SMaterial* m_material;
};
